-
Notifications
You must be signed in to change notification settings - Fork 5.3k
[WIP][clr-interp] Support for managed debugger breakpoints #123251
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag |
ed5ce4c to
dce1adc
Compare
- New InterpreterWalker class decodes bytecode control flow for stepping - Update TrapStep to use InterpreterWalker for interpreted code - Add per-thread TSNC_InterpreterSingleStep flag for step tracking - ApplyPatch now uses INTOP_SINGLESTEP vs INTOP_BREAKPOINT based on flag - Handle INTOP_SINGLESTEP in interpreter execution loop
- needed for step-in support in virtual calls
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This work-in-progress pull request adds support for managed debugger breakpoints in the CoreCLR interpreter. The changes extend the existing user breakpoint support (e.g., Debugger.Break()) to support IDE breakpoints and enable setting breakpoints when the program is stopped.
Changes:
- Adds interpreter single-step thread state flag and supporting methods
- Introduces new
INTOP_SINGLESTEPopcode for step-over operations - Implements
InterpreterWalkerto analyze interpreter bytecode for debugger stepping - Modifies breakpoint execution logic to distinguish between IDE breakpoints and step-out breakpoints
- Enables JIT completion notifications for interpreter code
- Pre-inserts IL offset 0 entry in the IL-to-native map to support method entry breakpoints
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| src/coreclr/vm/threads.h | Adds TSNC_InterpreterSingleStep thread state flag and related methods |
| src/coreclr/vm/jitinterface.cpp | Removes interpreter code exclusion from JITComplete notifications |
| src/coreclr/vm/interpexec.cpp | Implements breakpoint and single-step handling with opcode replacement |
| src/coreclr/vm/codeman.h | Adds IsInterpretedCode() helper method |
| src/coreclr/interpreter/intops.h | Adds helper functions to classify interpreter opcodes |
| src/coreclr/interpreter/inc/intops.def | Defines INTOP_SINGLESTEP opcode |
| src/coreclr/interpreter/compiler.cpp | Pre-inserts IL offset 0 mapping for method entry breakpoints |
| src/coreclr/debug/ee/interpreterwalker.h | Declares InterpreterWalker class for bytecode analysis |
| src/coreclr/debug/ee/interpreterwalker.cpp | Implements bytecode walker for debugger stepping operations |
| src/coreclr/debug/ee/functioninfo.cpp | Uses GetInterpreterCodeFromInterpreterPrecodeIfPresent for code address |
| src/coreclr/debug/ee/executioncontrol.h | Defines BreakpointInfo structure and GetBreakpointInfo method |
| src/coreclr/debug/ee/executioncontrol.cpp | Implements INTOP_SINGLESTEP patch support and breakpoint info retrieval |
| src/coreclr/debug/ee/controller.h | Includes interpreterwalker.h header |
| src/coreclr/debug/ee/controller.cpp | Implements TrapStep for interpreter using InterpreterWalker |
| src/coreclr/debug/ee/CMakeLists.txt | Adds interpreterwalker source files to build |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 19 out of 19 changed files in this pull request and generated 9 comments.
| #ifdef FEATURE_INTERPRETER | ||
|
|
||
| // Result of GetBreakpointInfo - combines opcode and step-out flag | ||
| struct BreakpointInfo | ||
| { | ||
| InterpOpcode originalOpcode; | ||
| bool isStepOut; | ||
| }; |
Copilot
AI
Jan 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The header file uses InterpOpcode type but doesn't include the header that defines it. While this works when included from files that already have the interpreter headers included (like interpexec.cpp), it violates header self-containment principles and could cause compilation errors if this header is included elsewhere. Add #include "../../interpreter/inc/intopsshared.h" after line 15 or before the BreakpointInfo struct definition to ensure the header is self-contained.
| // For FEATURE_INTERPRETER, the SP can also point to interpreter stack memory which is outside native stack bounds. | ||
| // Skip validation in these cases as the interpreter uses heap-allocated frame structures. | ||
| // TODO: Figure this out without disabling the check entirely. | ||
| #if !defined(TARGET_WASM) && !defined(FEATURE_INTERPRETER) | ||
| if (pRD->SP && pRD->_pThread) | ||
| { | ||
| #ifndef NO_FIXED_STACK_LIMIT | ||
| _ASSERTE(pRD->_pThread->IsExecutingOnAltStack() || PTR_VOID(pRD->SP) >= pRD->_pThread->GetCachedStackLimit()); | ||
| #endif // NO_FIXED_STACK_LIMIT | ||
| _ASSERTE(pRD->_pThread->IsExecutingOnAltStack() || PTR_VOID(pRD->SP) < pRD->_pThread->GetCachedStackBase()); | ||
| } | ||
| #endif // !TARGET_WASM | ||
| #endif // !TARGET_WASM && !FEATURE_INTERPRETER |
Copilot
AI
Jan 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Disabling the SP validation check for all FEATURE_INTERPRETER builds is overly broad and reduces debug coverage. The check should only be skipped when actually in interpreter frames, not for all code when the interpreter feature is enabled. Consider checking if the current frame is an interpreter frame (e.g., using EECodeInfo::IsInterpretedCode on the current IP) before skipping the validation, similar to how the TARGET_WASM case is handled. This would preserve the debug check for JIT code while still allowing interpreter frames to work correctly.
| { | ||
| // Indirect call (CALLVIRT, CALLI, CALLDELEGATE) - cannot determine target statically | ||
| // Use JMC backstop to catch method entry | ||
| // TODO: Could we do better? Why we can't use StubManagers to trace indirect calls? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can StubManagers if there is one that recognizes the code pattern being used to make the indirect call. If the indirect call doesn't need too many instructions to reach the destination you could also enable single-stepping and get there that way.
| const int32_t* skipIP = interpWalker.GetSkipIP(); | ||
| if (skipIP != NULL) | ||
| { | ||
| EnableSingleStep(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure why we'd enable single step here? The next line appears to set a breakpoint on the target already.
| const int32_t* nextIP = interpWalker.GetNextIP(); | ||
| if (nextIP != NULL) | ||
| { | ||
| EnableSingleStep(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
both breakpoint and single-stepping is unnecessary. If the breakpoint is where we expect execution to be after one instruction single step might be simpler.
| int32_t caseCount = interpWalker.GetSwitchCaseCount(); | ||
| LOG((LF_CORDB,LL_INFO10000,"DS::TS: INTOP_SWITCH with %d cases\n", caseCount)); | ||
|
|
||
| EnableSingleStep(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
its unnecessary to have both single-stepping and breakpoints on all the cases. If all the breakpoints are 1 instruction in the future perhaps its easier to rely on the single step rather than set all the breakpoints?
| const int32_t* nextIP = interpWalker.GetNextIP(); // branch target | ||
| const int32_t* skipIP = interpWalker.GetSkipIP(); // fallthrough | ||
|
|
||
| EnableSingleStep(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same as above
| const int32_t* skipIP = interpWalker.GetSkipIP(); | ||
| if (skipIP != NULL) | ||
| { | ||
| EnableSingleStep(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same thing
| (const BYTE*)(GetControlPC(&info->m_activeFrame.registers)))); | ||
| } | ||
|
|
||
| #ifdef FEATURE_INTERPRETER |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as a code factoring nit this function is already very long. I think it would be a little easier to follow if we factored some of this out into separate functions. Something like:
if(isInterpretter)
{
return TrapInterpretterCodeStep(...)
}
else
{
return TrapNativeCodeStep(...);
}
| *(uint32_t*)patch->address = INTOP_BREAKPOINT; | ||
| // Check if there is already a breakpoint patch at this address | ||
| uint32_t currentOpcode = *(uint32_t*)patch->address; | ||
| if (currentOpcode == INTOP_BREAKPOINT || currentOpcode == INTOP_SINGLESTEP) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems suspicious. the debugger could do something like this:
EnableSingleStepping()
ApplyPatch(nextAddress)
DisableSingleStepping()
The debugger would expect the breakpoint is still set at the end of that sequence but this code appears to avoid setting a breakpoint when single stepping is also enabled.
My guess is you either want one special opcode with state tracking on the side to describe what happens when you hit it, or you want three opcodes representing BREAKPOINT, SINGLESTEP, BREAKPOINT_AND_SINGLESTEP.
Also there is a comment lower down with broader questions about the single stepping abstraction. If we decide to go with the debugger having an interpretter single step emulator then I'd expect INTOP_SINGLESTEP goes away.
| int32_t opcode = *ip; | ||
|
|
||
| // If this is a breakpoint or single-step patch, get the original opcode from the patch table | ||
| if (opcode == INTOP_BREAKPOINT || opcode == INTOP_SINGLESTEP) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't expect to see entries in the debugger patch table when doing single stepping. That suggests the interpretter isn't really providing the normal single-stepping abstraction because the debugger doesn't track any patches when doing hardware single stepping.
I think we'd want one of:
- a complete single step abstraction implemented at the interpreter layer with no dependencies on the debugger patch table
- no single step at the interpretter layer and an InterpretterSingleStepEmulator at the debugger layer that implements single steps using interpretter breakpoints and predicting the next instruction as primitives.
Right now the implementation seems like a mixture of both, but closer to (2) than to (1).
| return true; | ||
| } | ||
|
|
||
| BreakpointInfo InterpreterExecutionControl::GetBreakpointInfo(const void* address) const |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll come back to this since I need to run for the moment, but I'm suspicious about the abstraction here. I think of these ExecutionControl APIs as being a layer below the debugger, something the debugger depends on. I did not expect them to be using the debugger patch table.
TODO: